/* Contabilidade Brasiliense - comportamento do site Compartilhado por todas as paginas. Cada bloco so roda se os elementos que ele usa existirem, porque nem toda pagina tem formulario, FAQ ou hero. */ // menu mobile (function () { var abrir = document.getElementById('open-menu'), fechar = document.getElementById('close-menu'), menu = document.getElementById('mobile-menu'), focoAnterior = null, timerFechar = 0, reduzirMovimento = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (!abrir || !fechar || !menu) return; function itensFocaveis() { return Array.prototype.filter.call( menu.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])'), function (item) { return item.offsetParent !== null; } ); } function ocultar() { menu.classList.add('hidden'); menu.classList.remove('flex'); } function set(aberto, devolverFoco) { clearTimeout(timerFechar); if (aberto) focoAnterior = document.activeElement; menu.setAttribute('aria-hidden', !aberto); menu.inert = !aberto; abrir.setAttribute('aria-expanded', aberto); document.body.style.overflow = aberto ? 'hidden' : ''; document.body.classList.toggle('menu-open', aberto); if (aberto) { menu.classList.remove('hidden'); menu.classList.add('flex'); // Separa display e opacidade para a transicao de entrada ser percebida. requestAnimationFrame(function () { menu.classList.add('menu-visible'); fechar.focus(); }); } else if (devolverFoco !== false && focoAnterior && document.contains(focoAnterior)) { focoAnterior.focus(); } if (!aberto) { menu.classList.remove('menu-visible'); if (reduzirMovimento) ocultar(); else timerFechar = setTimeout(ocultar, 210); } } abrir.onclick = function () { set(true); }; fechar.onclick = function () { set(false); }; menu.querySelectorAll('a').forEach(function (a) { a.onclick = function () { set(false, false); }; }); // Submenus acessiveis no celular. Servicos e Segmentos abrem sob demanda, // evitando uma lista longa assim que o menu ocupa a tela. menu.querySelectorAll('[data-mobile-submenu-toggle]').forEach(function (botao) { botao.addEventListener('click', function () { var painel = document.getElementById(botao.getAttribute('aria-controls')), icone = botao.querySelector('.mobile-submenu-icon'), abrirPainel = botao.getAttribute('aria-expanded') !== 'true'; if (!painel) return; botao.setAttribute('aria-expanded', abrirPainel ? 'true' : 'false'); botao.setAttribute('aria-label', (abrirPainel ? 'Fechar' : 'Abrir') + ' submenu'); painel.classList.toggle('hidden', !abrirPainel); if (icone) icone.classList.toggle('rotate-180', abrirPainel); }); }); document.addEventListener('keydown', function (e) { if (!menu.classList.contains('flex')) return; if (e.key === 'Escape') { set(false); return; } if (e.key !== 'Tab') return; var itens = itensFocaveis(), primeiro = itens[0], ultimo = itens[itens.length - 1]; if (!primeiro) return; if (e.shiftKey && document.activeElement === primeiro) { e.preventDefault(); ultimo.focus(); } else if (!e.shiftKey && document.activeElement === ultimo) { e.preventDefault(); primeiro.focus(); } }); window.addEventListener('resize', function () { if (window.innerWidth >= 1024 && menu.classList.contains('flex')) set(false, false); }, { passive: true }); })(); // A barra ganha um pouco mais de definicao depois que sai do topo, sem mudar // de tamanho e sem provocar deslocamento de layout. (function () { var header = document.querySelector('.site-header'); if (!header) return; var agendado = false; function atualizar() { header.classList.toggle('is-scrolled', window.scrollY > 16); agendado = false; } window.addEventListener('scroll', function () { if (agendado) return; agendado = true; requestAnimationFrame(atualizar); }, { passive: true }); atualizar(); })(); // reveal ao rolar + contadores animados (function () { var alvos = document.querySelectorAll('.reveal, .split'); if (!alvos.length) return; var reduzirMovimento = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; function contar(el) { var alvo = +el.dataset.target, sufixo = el.dataset.suffix || ''; if (el.dataset.contado === 'true' || !Number.isFinite(alvo)) return; el.dataset.contado = 'true'; if (reduzirMovimento) { el.textContent = alvo.toLocaleString('pt-BR') + sufixo; return; } var inicio = performance.now(), duracao = 1100; function passo(agora) { var p = Math.min((agora - inicio) / duracao, 1), eased = 1 - Math.pow(1 - p, 3); el.textContent = Math.round(alvo * eased).toLocaleString('pt-BR') + sufixo; if (p < 1) requestAnimationFrame(passo); } requestAnimationFrame(passo); } function exibir(el) { el.classList.remove('reveal-pending'); el.classList.add('in'); el.querySelectorAll('.count').forEach(contar); } if (reduzirMovimento || !('IntersectionObserver' in window)) { alvos.forEach(exibir); return; } var obs = new IntersectionObserver(function (entradas) { entradas.forEach(function (en) { if (!en.isIntersecting) return; exibir(en.target); obs.unobserve(en.target); }); }, { threshold: .12, rootMargin: '0px 0px -5% 0px' }); alvos.forEach(function (el) { var caixa = el.getBoundingClientRect(); if (el.classList.contains('in') || (caixa.top < window.innerHeight * .92 && caixa.bottom > 0)) { exibir(el); return; } el.classList.add('reveal-pending'); obs.observe(el); }); })(); // FAQ (accordion) document.querySelectorAll('.faq-t').forEach(function (botao, indice) { var painelInicial = botao.nextElementSibling; if (!painelInicial) return; if (!botao.id) botao.id = 'faq-botao-' + indice; painelInicial.setAttribute('aria-labelledby', botao.id); painelInicial.setAttribute('aria-hidden', 'true'); painelInicial.inert = true; botao.onclick = function () { var painel = botao.nextElementSibling, icone = botao.querySelector('.faq-i'), aberto = botao.getAttribute('aria-expanded') === 'true'; botao.setAttribute('aria-expanded', !aberto); painel.setAttribute('aria-hidden', aberto ? 'true' : 'false'); painel.inert = aberto; if (icone) { var uso = icone.querySelector('use'); if (uso) { var sprite = (uso.getAttribute('href') || '').split('#')[0]; uso.setAttribute('href', sprite + (aberto ? '#cb-i-lucide-plus' : '#cb-i-lucide-minus')); } } if (aberto) { painel.style.maxHeight = null; } else { painel.style.maxHeight = painel.scrollHeight + 'px'; } }; }); // Mantem a altura de respostas abertas correta quando o texto reflui ao girar // o aparelho ou quando a fonte termina de carregar. (function () { var timer; function ajustarFaqAberto() { document.querySelectorAll('.faq-t[aria-expanded="true"]').forEach(function (botao) { var painel = botao.nextElementSibling; if (painel) painel.style.maxHeight = painel.scrollHeight + 'px'; }); } window.addEventListener('resize', function () { clearTimeout(timer); timer = setTimeout(ajustarFaqAberto, 120); }, { passive: true }); if (document.fonts && document.fonts.ready) document.fonts.ready.then(ajustarFaqAberto); })(); // holofote que segue o mouse document.querySelectorAll('.spot').forEach(function (el) { if ((window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) || (window.matchMedia && window.matchMedia('(hover: none)').matches)) return; var frame = 0, x = 0, y = 0; el.addEventListener('pointermove', function (e) { x = e.clientX; y = e.clientY; if (frame) return; frame = requestAnimationFrame(function () { var r = el.getBoundingClientRect(); el.style.setProperty('--mx', (x - r.left) + 'px'); el.style.setProperty('--my', (y - r.top) + 'px'); frame = 0; }); }, { passive: true }); }); /* --------------------------------------------------------------------------- * MEDICAO * * cbTrack(evento, dados) empurra para o dataLayer e, quando existirem, para o * gtag (GA4 / Google Ads) e o fbq (Meta Pixel). Se nenhuma ferramenta estiver * configurada em partials/head.php, a funcao nao faz nada visivel - e de * proposito, para o site nunca quebrar por causa de tag ausente. * ------------------------------------------------------------------------- */ window.cbTrack = function (evento, dados) { dados = dados || {}; try { (window.dataLayer = window.dataLayer || []).push( Object.assign({ event: evento }, dados) ); if (typeof window.gtag === 'function') window.gtag('event', evento, dados); if (typeof window.fbq === 'function') { // Lead e evento padrao do Meta; o resto vai como evento customizado. if (evento === 'gerar_lead') window.fbq('track', 'Lead', dados); else window.fbq('trackCustom', evento, dados); } } catch (err) { /* medicao nunca pode derrubar a pagina */ } }; // Clique em qualquer CTA marcado com data-cta="..." document.addEventListener('click', function (e) { var alvo = e.target.closest('[data-cta]'); if (!alvo) return; var local = alvo.getAttribute('data-cta'), ehWhats = (alvo.href || '').indexOf('wa.me') > -1; window.cbTrack(ehWhats ? 'clique_whatsapp' : 'clique_cta', { local: local, rotulo: (alvo.textContent || '').trim().slice(0, 60), pagina: document.title }); }); // Estado compartilhado dos formularios: evita cliques duplicados e comunica // para tecnologias assistivas quando uma acao esta em andamento. window.cbAlternarEnvio = function (form, botao, ocupado, rotulo) { if (ocupado && form.dataset.enviando === 'true') return false; form.dataset.enviando = ocupado ? 'true' : 'false'; form.setAttribute('aria-busy', ocupado ? 'true' : 'false'); if (!botao) return true; if (ocupado) { botao.dataset.conteudoOriginal = botao.innerHTML; botao.disabled = true; botao.classList.add('opacity-60'); if (rotulo) botao.textContent = rotulo; } else { botao.disabled = false; botao.classList.remove('opacity-60'); if (botao.dataset.conteudoOriginal) botao.innerHTML = botao.dataset.conteudoOriginal; delete botao.dataset.conteudoOriginal; } return true; }; /* --------------------------------------------------------------------------- * COPIA DO LEAD PARA O N8N * * Chamada por TODOS os formularios do site. A URL vem de window.CB_WEBHOOK, * definida em partials/head.php - nenhum endereco fica escrito aqui. * * O envio nao trava o formulario: nao ha await, nao ha tela de erro e a falha e * engolida de proposito. O caminho principal do lead continua sendo o WhatsApp, * que abre logo em seguida; se o n8n estiver fora do ar, a pessoa nem percebe. * * Detalhes que parecem detalhe e nao sao: * application/json o n8n ja recebe os campos separados, sem precisar de um * passo de parse no fluxo. Custa uma requisicao de * verificacao (preflight) antes do envio, que o webhook * responde 204 liberando POST e content-type; * keepalive mantem o envio de pe mesmo com a aba abrindo o WhatsApp * no instante seguinte. * ------------------------------------------------------------------------- */ window.cbEnviarWebhook = function (formulario, dados) { var url = window.CB_WEBHOOK; if (!url || typeof fetch !== 'function') return; var corpo = { formulario: formulario, pagina: window.location.href, titulo: document.title, enviado_em: new Date().toISOString() }; for (var chave in dados) { if (Object.prototype.hasOwnProperty.call(dados, chave)) corpo[chave] = dados[chave]; } try { fetch(url, { method: 'POST', keepalive: true, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(corpo) })['catch'](function () {}); } catch (e) { /* sem rede, sem fetch: o WhatsApp ja resolve o contato */ } }; /* --------------------------------------------------------------------------- * TELEFONE * * cbTelefone.digitos() reduz o que foi digitado a DDD + numero (11 digitos no * maximo) DESCARTANDO ANTES os prefixos que a pessoa costuma colar junto: * zeros a esquerda (0 de operadora, 00 do internacional) e o 55 do codigo do * pais. Cortar em 11 sem tirar o prefixo era o bug antigo: "+55 61 99926-4714" * virava "(55) 61999-2647" - DDD errado e sem os dois ultimos digitos, ou seja, * lead impossivel de retornar. * * O 55 sai em dois casos: quando sobram mais de 11 digitos com ele, e quando * ha exatamente 11 mas o digito seguinte nao e 9. Celular brasileiro sempre * tem o 9 na frente do numero, entao "55" + algo que nao comeca com 9 nunca e * o DDD 55 - e codigo de pais com o numero ainda incompleto. * * Um celular real de Santa Maria/RS, (55) 9xxxx-xxxx, passa pelos dois testes * intacto: tem 11 digitos e tem o 9 no lugar certo. * ------------------------------------------------------------------------- */ window.cbTelefone = { digitos: function (valor) { // Nenhum DDD comeca com 0, entao zero a esquerda e sempre prefixo. var d = String(valor || '').replace(/\D/g, '').replace(/^0+/, ''); // Comecando com 55, o campo segura 13 digitos em vez de 11: e o espaco do // codigo do pais mais DDD e numero. Sem essa folga nao ha como distinguir // "55 + numero" de "DDD 55" enquanto a pessoa ainda digita. d = d.slice(0, d.slice(0, 2) === '55' ? 13 : 11); // Passou de 11 comecando com 55: o 55 so pode ser codigo de pais. if (d.length > 11 && d.slice(0, 2) === '55') d = d.slice(2); // Empate em 11 digitos: (55) 9xxxx-xxxx e Santa Maria/RS, mas celular // brasileiro SEMPRE tem 9 na frente do numero. Sem esse 9 o que esta ali e // codigo do pais com o numero pela metade - era o caso que a mascara // desenhava como se estivesse completo, e a pessoa enviava assim. if (d.length === 11 && d.slice(0, 2) === '55' && d.charAt(2) !== '9') d = d.slice(2); return d.slice(0, 11); }, mascara: function (valor) { var d = window.cbTelefone.digitos(valor); if (d.length <= 2) return d; if (d.length <= 6) return '(' + d.slice(0, 2) + ') ' + d.slice(2); var corte = d.length > 10 ? 7 : 6; return '(' + d.slice(0, 2) + ') ' + d.slice(2, corte) + '-' + d.slice(corte); } }; /* --------------------------------------------------------------------------- * FORMULARIO DE ANALISE DE CNPJ (landing de regularizacao) * * Destino: o atributo data-endpoint do